package org.coco24.midiutils

import java.io.File
import javax.sound.midi.*
import kotlin.experimental.or
import gnu.getopt.Getopt
import kotlin.test.assertFails

object InstrumentTracker {
    @JvmStatic
    fun main(args: Array<String>) {
        val getopt = Getopt("instrument_tracker", args, "i:o:")
        var inputFile: File? = null
        var outputFile: File? = null
        tailrec fun forEachOption(): Unit {
            val c = getopt.getopt()
            if (c != -1) {
                when (c.toChar()) {
                    'i' -> inputFile = File(getopt.optarg)
                    'o' -> outputFile = File(getopt.optarg)
                }
                forEachOption()
            }
        }
        forEachOption()
        inputFile?.also { file ->
            val sequence = process(MidiSystem.getSequence(inputFile))
            MidiSystem.write(sequence, 1, outputFile ?: File(file.parentFile, file.nameWithoutExtension + " (InstruTracks).mid"))
        }

    }

    fun process(sequence: Sequence): Sequence {
        val channel10ForPercussion = true //默认通道10为打击乐器专用
        val removeControllerEvent = false //移除所有控制器事件（滑音轮除外）
        val initializeControllers = true //在轨道开始以第一次该控制器设置的值初始化该控制器
        val initializePitchWheel = true //在轨道开始以第一次滑音轮的值初始化滑音轮
        val mergeSameInstrument = true //对相同乐器的轨道进行合并（控制器和滑音轮可能产生冲突）

        val events = HashMap<Byte, HashMap<Byte, ArrayList<MidiEvents>>>() //以音色库、音色分类的轨道（Bank -> Program -> Track）
        val metaEvents = ArrayList<MidiEvent>() //其他事件（BPM等）单独放置一个轨道
        val programs = HashMap<Byte, Byte>() //轨道当前的音色（Channel -> Program）
        var bank: Byte = 0 //当前的音色库

        sequence.tracks.copyOf().flatMap { track ->
            sequence.deleteTrack(track)
            (0 until track.size()).map { i -> track[i] }
        }.filterNot { removeControllerEvent && it.isControllerEvent() } //删除控制器事件
            .sortedWith(::midiEventComparator)
            .forEach { event ->
                if (event[0] == 0x00.toByte()) bank = event[1] //更换音色库
                else if (event.isChannelEvent()) { //通道消息
                    val channel = event.channel
                    val bank = if (channel10ForPercussion && channel == 9.toByte()) 128.toByte() else bank //手动设置通道10的音色库
                    val program = programs.getOrDefault(channel, 0.toByte())
                    val channelEvents = events.getOrPut(bank, { HashMap() }).getOrPut(program, { ArrayList() }).let {
                        it.find { mergeSameInstrument || it.firstOrNull { it.isChannelEvent() }?.channel?:-1 == channel }?:it.let {
                            val events = MidiEvents()
                            it.add(events)
                            events
                        }
                    }
                    if (event.isProgramChangeEvent()) { //更换音色
                        if (event.tick != 0L) //对更换音色时还处于noteOn的音符手动关闭（为了方便对通道之前所有的音符发送noteOff）
                            events.get(bank)?.get(program)?.also {
                                channelEvents.filter { it.isNoteOnEvent() }.map { it[1] }.toSet().forEach { note ->
                                    channelEvents.add(MidiEvent(ShortMessage((0x90.toByte() or channel).toInt(), note.toInt(), 0), event.tick))
                                }
                            }
                        programs[event.channel] = event[1]
                    } else channelEvents.add(event)
                } else metaEvents.add(event)
            }
        val buffer = events.mapValues { it.value.toList().sortedBy { it.first }.toMutableList() } //还未添加入序列的轨道 Bank -> Program -> ArrayList<MidiEvents>
        tailrec fun nextChannel(index: Int = 0) { //为下一个轨道寻找合适的通道并将事件添加进去
            if(buffer.values.fold(false, { acc, list ->
                    acc || list.fold(false, { acc, pair ->
                        acc || pair.second.isNotEmpty() }) })) { //判断是否还有未添加的轨道
                val channel = (index % 16).toByte()
                buffer.entries.find {
                        if (channel10ForPercussion) //如果通道10为打击乐器保留，则一般的轨道不能使用
                            if ((index % 16 + 1) % 10 == 0) it.key == 128.toByte()
                            else it.key != 128.toByte()
                        else it.value.isNotEmpty()
                    }?.value?.find { it.second.isNotEmpty() }?.also {
                    val track = sequence.createTrack()
                    val program = it.first
                    val events = it.second.removeFirst()
                    val programChangeMessage = ShortMessage()
                    programChangeMessage.setMessage((0xc0.toByte() or channel).toInt(), program.toInt(), 0)

                    events.add(MidiEvent(programChangeMessage, 0L))

                    if(initializeControllers) {
                        events.filter { it.isControllerEvent() }.toSetBy { it[1] }.forEach {
                            if (it.tick > 0L)
                                events.add(MidiEvent(it.message, 0L))
                        }
                    }
                    events.firstOrNull { it.isPitchWheelEvent() }?.also {
                        if(it.tick > 0L)
                            if(initializePitchWheel) events.add(MidiEvent(it.message, 0L))
                            else events.add(MidiEvent(ShortMessage(0xe0, 0x00, 0x40), 0L))
                    }

                    events.forEach {
                        if (it.isChannelEvent())
                            it.channel = channel
                    }
                    events.forEach {
                        track.add(it)
                    }
                    println("轨道${index + 1}\t音色$program")
                }
                nextChannel(index + 1)
            }
        }
        nextChannel()
        if (metaEvents.isNotEmpty()) sequence.createTrack().also { metaTrack -> metaEvents.forEach { metaTrack.add(it) } }
        return sequence
    }
}